﻿' ----------------------------------------------------------------------------------
' Microsoft Developer & Platform Evangelism
' 
' Copyright (c) Microsoft Corporation. All rights reserved.
' 
' THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
' EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 
' OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
' ----------------------------------------------------------------------------------
' The example companies, organizations, products, domain names,
' e-mail addresses, logos, people, places, and events depicted
' herein are fictitious.  No association with any real company,
' organization, product, domain name, email address, logo, person,
' places, or events is intended or should be inferred.
' ----------------------------------------------------------------------------------

Imports Microsoft.WindowsAzure.StorageClient
Imports Microsoft.WindowsAzure
Imports Microsoft.Samples.WindowsPhoneCloud.Web.UserAccountWrappers
Imports Microsoft.Samples.WindowsPhoneCloud.Web.Infrastructure
Imports System.ServiceModel.Web
Imports System.ServiceModel.Activation
Imports System.ServiceModel
Imports System.Net
Imports System.Globalization
Imports System
#If ACS Then
Imports Microsoft.IdentityModel.Claims
#End If
Namespace Services

    <ServiceBehavior(IncludeExceptionDetailInFaults:=False), AspNetCompatibilityRequirements(RequirementsMode:=AspNetCompatibilityRequirementsMode.Required)>
    Public Class SharedAccessSignatureService
        Implements ISharedAccessSignatureService

        Private Const ContainerSharedAccessPermissions As SharedAccessPermissions = SharedAccessPermissions.Write Or SharedAccessPermissions.Delete Or SharedAccessPermissions.List

        Private ReadOnly cloudBlobClient As CloudBlobClient
        Private ReadOnly webOperationContext As WebOperationContext

#If ACS Then
		Public Sub New()
			Me.New(Nothing, WebOperationContext.Current)
		End Sub

		<CLSCompliant(False)>
		Public Sub New(ByVal cloudBlobClient As CloudBlobClient, ByVal webOperationContext As WebOperationContext)
			If (cloudBlobClient Is Nothing) AndAlso (GetStorageAccountFromConfigurationSetting() Is Nothing) Then
				Throw New ArgumentNullException("cloudBlobClient", "The Cloud Blob Client cannot be null if no configuration is loaded.")
			End If

			Me.cloudBlobClient = If(cloudBlobClient, GetStorageAccountFromConfigurationSetting().CreateCloudBlobClient())
			Me.webOperationContext = webOperationContext
		End Sub

		Private ReadOnly Property UserId() As String
			Get
                Dim identity = TryCast(HttpContext.Current.User.Identity, IClaimsIdentity)
                Return identity.Claims.Single(Function(c) c.ClaimType = IdentityModel.Claims.ClaimTypes.NameIdentifier).Value
			End Get
		End Property
#Else
        Private ReadOnly context As HttpContextBase
        Private ReadOnly formsAuth As IFormsAuthentication
        Private ReadOnly membershipService As IMembershipService
        Private ReadOnly userPrivilegesRepository As IUserPrivilegesRepository

        Public Sub New()
            Me.New(Nothing, New HttpContextWrapper(HttpContext.Current), New UserTablesServiceContext(), New FormsAuthenticationService(), New AccountMembershipService(), webOperationContext.Current)
        End Sub

        <CLSCompliant(False)>
        Public Sub New(ByVal cloudBlobClient As CloudBlobClient, ByVal context As HttpContextBase, ByVal userPrivilegesRepository As IUserPrivilegesRepository, ByVal formsAuth As IFormsAuthentication, ByVal membershipService As IMembershipService, ByVal webOperationContext As WebOperationContext)
            If (context Is Nothing) AndAlso (HttpContext.Current Is Nothing) Then
                Throw New ArgumentNullException("context", "The context cannot be null if not running on a Web context.")
            End If

            If (cloudBlobClient Is Nothing) AndAlso (GetStorageAccountFromConfigurationSetting() Is Nothing) Then
                Throw New ArgumentNullException("cloudBlobClient", "The Cloud Blob Client cannot be null if no configuration is loaded.")
            End If

            Me.cloudBlobClient = If(cloudBlobClient, GetStorageAccountFromConfigurationSetting().CreateCloudBlobClient())
            Me.context = context
            Me.userPrivilegesRepository = userPrivilegesRepository
            Me.formsAuth = formsAuth
            Me.membershipService = membershipService
            Me.webOperationContext = webOperationContext
        End Sub

        Private ReadOnly Property UserId() As String
            Get
                Dim ticketValue As String = Nothing

                Dim cookie = Me.context.Request.Cookies(Me.formsAuth.FormsCookieName)
                If cookie IsNot Nothing Then
                    ' From cookie.
                    ticketValue = cookie.Value
                ElseIf Me.context.Request.Headers("AuthToken") IsNot Nothing Then
                    ' From HTTP header.
                    ticketValue = Me.context.Request.Headers("AuthToken")
                End If

                If Not String.IsNullOrEmpty(ticketValue) Then
                    Dim ticket As FormsAuthenticationTicket

                    Try
                        ticket = Me.formsAuth.Decrypt(ticketValue)
                    Catch
                        Throw New WebFaultException(Of String)("The authorization ticket cannot be decrypted.", HttpStatusCode.Unauthorized)
                    End Try

                    If ticket IsNot Nothing Then
                        ' Authorize blobs usage.
                        Dim userIdValue = Me.membershipService.GetUser(New FormsIdentity(ticket).Name).ProviderUserKey.ToString()
                        If Not Me.userPrivilegesRepository.HasUserPrivilege(userIdValue, PrivilegeConstants.BlobsUsagePrivilege) Then
                            Throw New WebFaultException(Of String)("You have no permission to use blobs.", HttpStatusCode.Unauthorized)
                        End If

                        Return userIdValue
                    Else
                        Throw New WebFaultException(Of String)("The authorization token is no longer valid.", HttpStatusCode.Unauthorized)
                    End If
                Else
                    Throw New WebFaultException(Of String)("Resource not found.", HttpStatusCode.NotFound)
                End If
            End Get
        End Property
#End If

        Public Function GetContainerSharedAccessSignature() As Uri Implements ISharedAccessSignatureService.GetContainerSharedAccessSignature
            ' Authenticate.
            Dim userId = Me.UserId

            Try
                ' Each user has its own container.
                Dim container = Me.GetUserContainer(userId)
                Dim containerSASExperiationTime = Integer.Parse(ConfigReader.GetConfigValue("ContainerSASExperiationTime"), NumberStyles.Integer, CultureInfo.InvariantCulture)
                Dim sas = container.GetSharedAccessSignature(New SharedAccessPolicy() With {.Permissions = ContainerSharedAccessPermissions, .SharedAccessExpiryTime = Date.UtcNow + TimeSpan.FromMinutes(containerSASExperiationTime)})

                If Me.webOperationContext IsNot Nothing Then
                    Me.webOperationContext.OutgoingResponse.Headers.Add("Cache-Control", "no-cache")
                End If

                Dim uriBuilder = New UriBuilder(container.Uri) With {.Query = sas.TrimStart("?"c)}
                Return uriBuilder.Uri
            Catch exception As Exception
                Throw New WebFaultException(Of String)(exception.Message, HttpStatusCode.InternalServerError)
            End Try
        End Function

        Public Function GetBlobsSharedAccessSignatures(ByVal blobPrefix As String, ByVal useFlatBlobListing As Boolean) As Models.CloudBlobCollection Implements ISharedAccessSignatureService.GetBlobsSharedAccessSignatures
            ' Authenticate.
            Dim userId = Me.UserId

            If Not String.IsNullOrEmpty(blobPrefix) Then
                blobPrefix = blobPrefix.TrimStart("/"c, "\"c).Replace("\"c, "/"c)
            End If

            Try
                ' Each user has its own container.
                SetReadOnlySharedAccessPolicy(Me.GetUserContainer(userId))
                Dim prefix = String.Format(CultureInfo.InvariantCulture, "{0}/{1}", GetUserContainerName(userId), blobPrefix)

                Dim blobs = Me.cloudBlobClient.ListBlobsWithPrefix(prefix, New BlobRequestOptions With {.UseFlatBlobListing = useFlatBlobListing})
                Dim result = New Models.CloudBlobCollection With {.Blobs = blobs.Where(Function(b) TypeOf b Is CloudBlob).Select(Function(b) b.ToModel(GetUserContainerName(userId), Me.cloudBlobClient.Credentials.AccountName)).ToArray()}

                If Me.webOperationContext IsNot Nothing Then
                    Me.webOperationContext.OutgoingResponse.Headers.Add("Cache-Control", "no-cache")
                End If

                Return result
            Catch exception As Exception
                Throw New WebFaultException(Of String)(exception.Message, HttpStatusCode.InternalServerError)
            End Try
        End Function

        Private Shared Sub SetReadOnlySharedAccessPolicy(ByVal container As CloudBlobContainer)
            Dim blobSASExperiationTime = Integer.Parse(ConfigReader.GetConfigValue("BlobSASExperiationTime"), NumberStyles.Integer, CultureInfo.InvariantCulture)
            Dim permissions = container.GetPermissions()
            Dim options = New BlobRequestOptions With {.AccessCondition = AccessCondition.IfMatch(container.Properties.ETag)}
            ' Fail if someone else has already changed the container before we do.
            Dim sharedAccessPolicy = New SharedAccessPolicy With {.Permissions = SharedAccessPermissions.Read, .SharedAccessExpiryTime = Date.UtcNow + TimeSpan.FromDays(blobSASExperiationTime)}

            permissions.SharedAccessPolicies.Remove("readonly")
            permissions.SharedAccessPolicies.Add("readonly", sharedAccessPolicy)

            container.SetPermissions(permissions, options)
        End Sub

        Private Shared Function GetStorageAccountFromConfigurationSetting() As CloudStorageAccount
            Dim account As CloudStorageAccount = Nothing

            Try
                account = CloudStorageAccount.FromConfigurationSetting("DataConnectionString")
            Catch e1 As InvalidOperationException
                account = Nothing
            End Try

            Return account
        End Function

        Private Shared Function GetUserContainerName(ByVal userId As String) As String
            ' The container name for the user contains a hash of the user Id.
            Dim containerName = String.Format(CultureInfo.InvariantCulture, "usercontainer{0}", userId.GetHashCode())

            Return containerName.ToLowerInvariant()
        End Function

        Private Function GetUserContainer(ByVal userId As String) As CloudBlobContainer
            Dim container = Me.cloudBlobClient.GetContainerReference(GetUserContainerName(userId))
            container.CreateIfNotExist()

            Return container
        End Function
    End Class
End Namespace